/+
Library dbapi - Simple database api for the D programming language.

Copyright (C) 2007 Myron Alexander (myron.alexander...gmail.com)

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

module dbapi.sqlite3;

import std.stdarg;
import std.boxer;
import std.stdio;
import std.string;
import std.traits;
import std.c.string;

import dbapi.base;

import sqlite.sqlite3;

alias std.string.toString str;

//------------------------------------------------------------------------------
// SECTION: Public module global constants
//------------------------------------------------------------------------------

const char[] apilevel      = "2.0";
const int    threadsafety  = 0;
const char[] paramstyle    = "qmark";

/**
 * Request connection to an in-memory database without any temporary file.
 */
const char[] IN_MEMORY_DB  = ":memory:";

/**
 * Request connection to a temporary database that acts like an in-memory
 * database except that the database is created as a temporary file.
 */
const char[] TMP_DB        = "";


enum TransactionMode {
     /**
      * Auto-commit transaction mode will not create a transaction context
      * and each statement will be automatically committed at completion.
      *
      * The exception to this is that the executemany method will
      * create a transaction context for the bulk operation and commit
      * at the end. This is for performance purposes.
      *
      * The transaction type is ignored.
      */
     AUTO_COMMIT = 0

     /**
      * The user assumes control over transactions. This is similar to
      * AUTO_COMMIT but allows the developer to create their own transactions
      * using BEGIN statements. The interface will not handle any transaction
      * logic.
      *
      * The transaction type is ignored.
      */
   , USER

     /**
      * A transaction will be created with a statement that modifies the data
      * such as INSERT, UPDATE, DELETE. The transaction will be closed when
      * a commit or rollback statement is issued and will be recreated on
      * next modify statement.
      *
      * Transaction type determines what transaction mechanism is used.
      */
   , ON_MODIFY

     /**
      * The transaction context is created at the start of the connection.
      * A commit or rollback will end the context and create a new one.
      *
      * Transaction type determines what transaction mechanism is used.
      */
   , ALWAYS
}

enum TransactionType {
     /**
      * The default transaction type specified by the database.
      */
     DEFAULT = 0

     /**
      * Deferred means that no locks are acquired on the database until the
      * database is first accessed. Thus with a deferred transaction, the
      * BEGIN statement itself does nothing. Locks are not acquired until the
      * first read or write operation. The first read operation against a
      * database creates a SHARED lock and the first write operation creates a
      * RESERVED lock. Because the acquisition of locks is deferred until they
      * are needed, it is possible that another thread or process could create
      * a separate transaction and write to the database after the BEGIN on the
      * current thread has executed.
      */
   , DEFERRED
   
     /**
      * If the transaction is immediate, then RESERVED locks are acquired on all
      * databases as soon as the BEGIN command is executed, without waiting for
      * the database to be used. After a BEGIN IMMEDIATE, you are guaranteed
      * that no other thread or process will be able to write to the database
      * or do a BEGIN IMMEDIATE or BEGIN EXCLUSIVE. Other processes can continue
      * to read from the database, however.
      */
   , IMMEDIATE
   
     /**
      * An exclusive transaction causes EXCLUSIVE locks to be acquired on all
      * databases. After a BEGIN EXCLUSIVE, you are guaranteed that no other
      * thread or process will be able to read or write the database until
      * the transaction is complete.
      */
   , EXCLUSIVE
}


enum BatchResult {
     SUCCESS_NO_INFO = -2
   , EXECUTE_FAILED  = -3
}


/+

When the transaction mode is AUTO_COMMIT:

  * Transaction type is ignored.
  * Creating statements with transaction QL (BEGIN,...) are invalid.
  * Commit/Rollback does nothing and the statements created but for use by
    execytemany.
  * When ROLLBACK resolution conflict is enacted, the interface does nothing.
  * When a DDL statement is created, the interface does nothing.
  * For executemany, a transaction is created, all the iterations are executed
    and then the transaction is committed and the method ends.

When the transaction mode is USER:

  * Transaction type is ignored.
  * Creating statements with transaction QL (BEGIN,...) are allowed.
  * Commit/Rollback does nothing and the statements are not created.
  * When ROLLBACK resolution conflict is enacted, the interface does nothing.
  * When a DDL statement is created, the interface does nothing.
  * With executemany, the interface does not perform any transaction logic.

When the transaction mode is ON_MODIFY:

  * When transaction type is :
    - DEFAULT   : "BEGIN"
    - DEFERRED  : "BEGIN DEFERRED"
    - IMMEDIATE : "BEGIN IMMEDIATE"
    - EXCLUSIVE : "BEGIN EXCLUSIVE"
  * Creating statements with transaction QL (BEGIN,...) are invalid.
  * The transaction is created when a STATEMENT_MODIFY statement is executed and
    a transaction is not currently open.
  * When ROLLBACK conflict resolution is triggered, the interface does nothing.
  * When a DDL statement is executed, the interface commits an open transaction
    before the statement is executed. A tranasction is not started.
  * For executemany, if no transaction was started, then a new transaction is
    started. On completion of the method, the transaction is not committed or
    rolled back. That is for the user to do.

When the transaction mode is ALWAYS:

  * When transaction type is as per ON_MODIFY.
  * Creating statements with transaction QL (BEGIN,...) are invalid.
  * The transaction is created at the start of the connection.
  * When ROLLBACK conflict resolution is triggered, the interface creates a new
    transaction.
  * When a DDL statement is executed, the interface commits an open transaction
    before the statement is executed and then creates a new transaction.
  * For executemany, only if the conflict resolution causes a rollback will
    transaction logic be performed. In that case, a new transaction is created.
+/

//------------------------------------------------------------------------------
// SECTION: Public interface methods
//------------------------------------------------------------------------------

/**
 * Open a connection to the dsn provided.
 *
 * Returns:
 *    Connection object wrapping a valid connection to the sqlite database.
 */
Connection connect (
        char[]             dsn
      , TransactionMode    transactionMode = TransactionMode.AUTO_COMMIT
      , TransactionType    transactionType = TransactionType.DEFAULT)
{
   Connection c =
      new Connection (
         dsn, transactionMode, transactionType, standardModeHandlerFactory);
   return c;
}


/**
 * Process the given text to preserve meaning when included in the SQL
 * statement.
 *
 * When text is included in the SQL statement directly, there are rules that
 * must be observed to ensure that the text retains meaning and that the text
 * does not introduce SQL syntax errors. For this function, single quotes
 * within the text are quoted by doubling such that one quote "'" becomes two
 * "''".
 *
 * Params:
 *
 *  s = text to be escaped.
 *
 * Returns:
 *   String with single quotes escaped with single quotes.
 */
char[] escapeText (char[] s) {
   return std.string.replace (s, "'", "''");
}

unittest {
   char[] rs;
   
   rs = escapeText ("A string without quotes");
   assert ("A string without quotes" == rs);
   
   rs = escapeText ("'A string with one starting quote");
   assert ("''A string with one starting quote" == rs);

   rs = escapeText ("A string with one trailing quote'");
   assert ("A string with one trailing quote''" == rs);
   
   rs = escapeText ("'A string with bracketing quotes'");
   assert ("''A string with bracketing quotes''" == rs);
   
   rs = escapeText ("A string with ' an embedded quote");
   assert ("A string with '' an embedded quote" == rs);
   
   rs = escapeText ("A string with '' two embedded quotes");
   assert ("A string with '''' two embedded quotes" == rs); 
}

//------------------------------------------------------------------------------
// SECTION: Public interface classes
//------------------------------------------------------------------------------

class Connection {

   //---------------------------------------------------------------------------
   // Constructors
   //---------------------------------------------------------------------------

   private this (
           char[]                         dsn
         , TransactionMode                transactionMode
         , TransactionType                transactionType
         , TransactionModeHandlerFactory  modeFactory      )
   {
      open (dsn, transactionMode, transactionType, modeFactory);
   }

   private ~this() {
      debug {
         writefln ("Connecting destructing");
      }

      if (!close ()) {
         throw new SqlInterfaceException (
            "Unable to close database '" ~ m_dsn ~
            "', there are statements that have "
            "not been closed.", null, SQLITE_BUSY);
      }
   }

   //---------------------------------------------------------------------------
   // Interface methods
   //---------------------------------------------------------------------------

   /**
    * Close the connection.
    *
    * Returns:
    *   true when the connection has been closed or was closed.
    *   false when the connection cannot be closed due to open statements.
    */
   public bool close () {

      /* TODO - Check if it is possible to close the connection before
       *        attempting to close.
       */

      if (!isClosed ()) {

         /* The closing procedure has been initiated. No further operations
          * on this connection will be permitted except for calls to this
          * close method.
          */
         m_isClosing = true;

         if (m_transModeHandler !is null) {
            /* The rollback is dependent on the transaction mode handler and is
             * not guaranteed to actually cause a rollback.
             */
            m_transModeHandler.rollback ();
            delete m_transModeHandler;
         }

         int rc = sqlite3_close (m_dbcon);

         switch (rc) {
            case SQLITE_OK:
               setConnectionClosed ();
               break;

            case SQLITE_BUSY:
               return false;
               break;

            default:
               throw SqliteError!(SqlOperationalException) (
                  m_dbcon,
                  "Error on closing database '" ~ m_dsn ~ "'. "
                  "Sqlite error ($(errmsg)).");
         }
      }

      return true;
   }

   /**
    * Commit any pending transaction to the database.
    *
    * If the database cannot be committed because it is busy, then false
    * is returned.
    *
    * Returns:
    *   true  when the commit completed ok.
    *   false when the commit could not be executed now as the database is
    *         locked. Clear the lock and try again.
    */
   public bool commit () {
      checkClosing (this);
      return m_transModeHandler.commit ();
   }

   /**
    * Undo all actions on a transaction.
    */
   public void rollback () {
      checkClosing (this);
      m_transModeHandler.rollback ();
   }

   /**
    * Return a new Cursor Object using the connection.
    */
   public Cursor cursor () {
      checkClosing (this);
      return new Cursor (this);
   }

   //---------------------------------------------------------------------------
   // Properties
   //---------------------------------------------------------------------------

   /**
    * Property to get the internal sqlite3* handle.
    */
   public sqlite3* dbcon () {
      checkClosing (this);
      return m_dbcon;
   }


   /**
    * Property to get the transaction state.
    *
    * Returns:
    *   true when a transaction has been started, otherwise false.
    */
   public bool inTransaction () {
      checkClosing (this);
      return m_transModeHandler.inTransaction ();
   }

   //---------------------------------------------------------------------------
   // Additional non-dbapi specified public methods
   //---------------------------------------------------------------------------

   /**
    * Test connection status, returns true when open otherwise false.
    */
   public bool isOpen () {
      return !m_isClosing && m_dbcon !is null;
   }

   /**
    * Test connection status, returns true when closed otherwise false.
    */
   public bool isClosed () {
      return m_dbcon is null;
   }

   //---------------------------------------------------------------------------
   // Internal methods
   //---------------------------------------------------------------------------

   private void open (
           char[]                         dsn
         , TransactionMode                transactionMode
         , TransactionType                transactionType 
         , TransactionModeHandlerFactory  modeFactory       )
   {
      m_dsn             = dsn;
      m_transactionMode = transactionMode;
      m_transactionType = transactionType;

      //
      // Create the connection.
      //
      
      int rc = sqlite3_open (toStringz (dsn), &m_dbcon);

      scope (failure) setConnectionClosed ();

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_dbcon,
            "Error on opening database '" ~ dsn ~
            "'. Sqlite error ($(errmsg)).");
      }

      //
      // Set the transaction mode handler.
      //
      
      m_transModeHandler =
         modeFactory.createTransactionModeHandler (
            m_dbcon, m_transactionMode, m_transactionType);
   }



   /**
    * Called after the connection is closed to set the indicator to the closed
    * state.
    */
   private void setConnectionClosed () {
      m_dbcon = null;
   }



   private char[]                   m_dsn;

   private sqlite3*                 m_dbcon;

   private TransactionModeHandler   m_transModeHandler;
   private TransactionMode          m_transactionMode;
   private TransactionType          m_transactionType;
      
   private bool                     m_isClosing = false;
}




public class Cursor {
   private Connection m_connection;
   private Statement  m_currentstmt;

   /* True when a row is available for fetching otherwise false.
    */
   private bool       m_rowAvailable;

   /* True when the statement has been executed but before the first
    * fetch.
    */
   private bool       m_newExecution;

   /* The number of rows returned as an array for fetchmany and the array growth
    * value for fetchall.
    */
   private int        m_arraysize = 10;


   //---------------------------------------------------------------------------
   // Constructors
   //---------------------------------------------------------------------------


   private this (Connection connection) {
      m_connection = connection;
   }


   private ~this () {
      close ();
   }


   //---------------------------------------------------------------------------
   // Interface properties
   //
   //    arraysize   (RW)
   //    connection  (R)
   //    description (R)
   //    lastrowid   (R)
   //    rowcount    (R)
   //---------------------------------------------------------------------------

   public int arraysize () {
      checkClosing (m_connection);
      return m_arraysize;
   }


   public int arraysize (int value) {
      checkClosing (m_connection);
      if (value <= 0) {
         throw new SqlInterfaceException (
            "Invalid arraysize value specified. The size must be a non-zero, "
            "positive, value. Value specified (" ~ str(value) ~ ").");
         
      }

      return m_arraysize = value;
   }


   public ColumnAttributes[] description () {
      checkClosing (m_connection);

      /* When the no statement has been executed, return null.
       */
      if (m_currentstmt is null) return null;

      /* Request the number of columns that the statement will return.
       */
      int colcount = m_currentstmt.getStmtColCount ();

      /* If the statement is of a type that does not return a value, then
       * return null.
       */
      if (colcount == 0) return null;

      ColumnAttributes[] attributes = new ColumnAttributes[colcount];

      for (int i = 0; i < colcount; i++) {
         ColumnAttributes attr;
         attr.name = m_currentstmt.getStmtColName (i);
         fillTypeFromStmtColType (
               toupper (m_currentstmt.getStmtColType (i)), attr);
         attributes [i] = attr;
      }

      return attributes;
   }


   public int lastrowid () {
      assert (false, "Not implemented yet");
      checkClosing (m_connection);
   }


   public int rowcount ()
   in {
      assert (false, "rowcount property not implemented yet.");
   }
   body {
      checkClosing (m_connection);
      return -1;
   }


   //---------------------------------------------------------------------------
   // Interface methods
   //---------------------------------------------------------------------------

   
   public void execute (char[] operation, ...) {

      checkClosing (m_connection);

      createStatement (operation);

      int providedBindParmsCnt = _arguments.length;
      int expectedBindParmsCnt = m_currentstmt.numBindParameters;

      if (expectedBindParmsCnt > 0) {
         if (providedBindParmsCnt < expectedBindParmsCnt) {
            throw new SqlProgrammingException (
               "Cannot execute statement as there are parameters of the SQL "
               "statement for which no value have been provided. Expecting (" ~
               str (expectedBindParmsCnt) ~ ") parameter values but were "
               "provided (" ~ str (providedBindParmsCnt) ~
               ") values. Error on statement (" ~ operation ~ ").");
            
         } else if (providedBindParmsCnt > expectedBindParmsCnt) {
            throw new SqlProgrammingException (
               "Cannot execute statement as the number of parameter values "
               "exceeds the number of parameters. Expecting (" ~
               str (expectedBindParmsCnt) ~ ") parameter values but were "
               "provided (" ~ str (providedBindParmsCnt) ~
               ") values. Error on statement (" ~ operation ~ ").");
         }

         bindParameters (_arguments, _argptr);
      }

      m_connection.m_transModeHandler.startExecute (m_currentstmt.statementType);

      {
         /* If the statement execute fails, the transaction handler error
          * method must be called. This block is done in it's own scope level
          * so that the scope guard will only execute if the step fails and not
          * if the endExecute fails.
          */
         scope (failure) m_connection.m_transModeHandler.executionError ();
            
         /* Execute the statement. If the statement is a query that results in
          * data, the step() function will return true.
          */
         m_rowAvailable = m_currentstmt.step ();
      }

      m_connection.m_transModeHandler.endExecute ();

      m_newExecution = true;
   }

   
   private void createStatement (char[] operation) {
      if (m_currentstmt is null) {
         m_currentstmt = new Statement (m_connection, operation);

      } else if (m_currentstmt.isClosed () || m_currentstmt.text != operation) {
         delete m_currentstmt;
         m_currentstmt = new Statement (m_connection, operation);

      } else {
         m_currentstmt.reset ();
      }
   }


   /**
    * Execute a batch update operation on given values.
    *
    * This method can be called with two different signatures:
    *
    *   executemany ("...", Box[]...)
    *     - Each box array is taken as a row with multiple values.
    *     - The number of values within the Box[] must exactly correlate with
    *       the number of parameters in the SQL. The only clarification for this
    *       is that, for named parameters, there must be one value per name but
    *       that name may appear more than once in the SQL.
    *     - The value types supported is the same as for execute.
    *
    *   executemany ("...", ...)
    *     - Each parameter is taken as a single value row.
    *     - The SQL must have exactly one positional, or named, parameter.
    *     - The value types supported is the same as for execute.
    *     - You can also pass in single value boxes mixed with other parameters.
    *       This allows you to use values from a prior query without unboxing
    *       them.
    *
    * The method calls executemany (operation, Box[][]) so the operation and
    * result will be the same.
    */
   int[] executemany (char[] operation, ...) {

      checkClosing (m_connection);
      
      if (0 == _arguments.length) {
         throw new SqlProgrammingException (
            "Invalid number of parameter values. Zero parameter values were "
            "specified but an executemany operation requires at least one "
            "parameter. Error on statement (" ~ operation ~ ").");
      }

      Box[][] values;
      
      if (_arguments[0] == typeid (Box[])) {
         //
         // Create multiple value row box array.
         //

         debug {writefln ("Creating array of box[]");}
         
         values = new Box[][_arguments.length];

         foreach (i, t; _arguments) {
            if (typeid (Box[]) != t) {
               throw new SqlProgrammingException (
                  "Invalid bind type. Mixing single and multiple value rows. If "
                  "the first parameter value is Box[], then it is "
                  "assumed that the rest of the parameters are multiple value rows. "
                  "Error on parameter (" ~ str (i) ~ "). SQL (" ~ operation ~
                  ").");
            }
            
            values[i] = va_arg!(Box[])(_argptr);
         }
         
      } else {
         //
         // Create single value row box array.
         //

         debug {writefln ("Creating array of other");}

         va_list data = _argptr;

         values = new Box[][](_arguments.length, 1);

         foreach (i, t; _arguments) {
            if (typeid (Box[]) == t) {
               throw new SqlProgrammingException (
                  "Invalid bind type. Mixing single and multiple value rows. If "
                  "the first parameter value is other than Box[], then it is "
                  "assumed that the rest of the parameters are single value rows. "
                  "Error on parameter (" ~ str (i) ~ "). SQL (" ~ operation ~
                  ").");
               
            } else if (typeid (Box) == t) {
               values[i][0] = *cast(Box*)data;
               
            } else {
               values[i][0] = box (t, data);
            }

            data = nextArg (t, data);
         }
      }

      return executemanyImpl (operation, values);
   }


   int[] executemany (char[] operation, Box[][] values)
   body {

      checkClosing (m_connection);

      //
      //
      //

      /* Validate that a non-null array was passed in.
       */
      if (values is null) {
         throw new SqlProgrammingException (
            "Invalid parameter value array. The reference passed in is null "
            "but a populated array is required. Error on statement (" ~
            operation ~ ").");
      }

      /* Validate that there is at least one parameter row.
       */
      if (0 == values.length) {
         throw new SqlProgrammingException (
            "Invalid number of parameter values. Zero parameter values were "
            "specified but an executemany operation requires at least one "
            "parameter. Error on statement (" ~ operation ~ ").");
      }

      //
      //
      //

      return executemanyImpl (operation, values);
   }



   private int[] executemanyImpl (char[] operation, Box[][] values)
   in {
      assert (operation.length > 0);
      assert (values.length > 0);
   }
   body {

      createStatement (operation);

      //
      //
      //

      if (StatementType.STATEMENT_MODIFY != m_currentstmt.statementType) {
         throw new SqlProgrammingException (
            "Invalid statement type. The only permittable statements for "
            "batch operation are those that modify data such as INSERT, "
            "UPDATE, DELETE, and REPLACE. Error on statement (" ~ operation ~
            ").");
      }

      int expectedBindParmsCnt = m_currentstmt.numBindParameters;

      if (0 == expectedBindParmsCnt) {
         throw new SqlProgrammingException (
            "Invalid number of parameters specified. Zero parameters were "
            "specified in the SQL but an executemany operation requires at "
            "least one parameter. Error on statement (" ~ operation ~ ").");
      }

      //
      //
      //

      /* Validate that all the parameter rows are non-null and that the required
       * number of parameter values are present.
       */
      foreach (i, v; values) {
         if (v is null) {
            throw new SqlProgrammingException (
               "Invalid parameter value array. The value array for row (" ~
               str (i) ~ ") is null but a populated row array is required. "
               "Error on statement (" ~ operation ~ ").");

         } else if (expectedBindParmsCnt != v.length) {
            throw new SqlProgrammingException (
               "Invalid parameter value array. The number of parameter values "
               "do not match the number of parameters. Expecting (" ~ 
               str (expectedBindParmsCnt) ~ ") values but received (" ~ 
               str (v.length) ~ "). Error on parameter row (" ~ str (i) ~ 
               ") for statement (" ~ operation ~ ").");
         }

         foreach (j, w; v) {
            if (w.type is null) {
               throw new SqlProgrammingException (
                  "Invalid parameter value array. Found uninitialised box "
                  "value. Error on parameter row (" ~ str (i) ~ 
                  "), column (" ~ str (j) ~ "), for statement (" ~ operation ~
                  ").");
            }
         }
      }

      //
      //
      //

      m_connection.m_transModeHandler.startBatch ();

      //
      //
      //
      
      int[] recordsAffected = new int[values.length];
      
      foreach (i, pa; values) {

         bool hasrow;

         try {
            m_currentstmt.reset ();
            bindParameters (pa);
            hasrow = m_currentstmt.step ();

         } catch (SqlException e) {
            m_connection.m_transModeHandler.batchError ();
            recordsAffected[i] = BatchResult.EXECUTE_FAILED;
            throw new BatchUpdateException (e, recordsAffected[0..i+1]);
         }
         
         assert (!hasrow,
            "Queries are not allowed as batch statements. This should have "
            "been check before the statement is executed.");

         recordsAffected[i] = m_currentstmt.numChanges ();
      }


      //
      //
      //

      m_connection.m_transModeHandler.endBatch ();

      //
      //
      //

      return recordsAffected;
   }
   

   private bool moveCursorToNextAvailableRecord () {

      checkClosing (m_connection);

      /* If no row is available then exit now.
       */
      if (!m_rowAvailable) return false;

      /* On execute, the statement is compiled and the first step is performed
       * so no step operation must be done on a newly executed statement.
       */
      if (m_newExecution) {
         m_newExecution = false;

      } else {
         m_rowAvailable = m_currentstmt.step ();
      }
      
      return m_rowAvailable;
   }


   public Box[] fetchone () {

      checkClosing (m_connection);

      /* Move the cursor to the next unread result row. If no row is
       * available then exit now.
       */
      if (!moveCursorToNextAvailableRecord ()) return null;

      int colcount = m_currentstmt.getRsColCount ();

      /* When the last step operation returned a record but the column count is
       * zero, something unexpected is happening so throw an exception.
       */
      if (0 == colcount) {
         m_rowAvailable = false;
         throw new SqlInterfaceException (
            "Unable to fetch a record. The operation to retrieve the next data "
            "row reported a row available but the number of columns reported "
            "is zero. This should never happen and is either a bug in this "
            "inteface, a bug in the SQLite implementation, or the way the "
            "SQLite implementation works has changed. Either way, unable to "
            "continue. ");
      }

      Box[] row = new Box[colcount];

      for (int i = 0; i < colcount; i++) {
         row [i] = m_currentstmt.getValue (i);
      }

      return row;
   }


   public Box[][] fetchmany (int size = -1)
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosing (m_connection);

      if (-1 != size && 0 >= size) {
         throw new SqlInterfaceException (
            "Invalid size for fetchmany. The size parameter must be either -1, "
            "to use arraysize, or a number greater than zero.");
      }

      if (-1 == size) {
         size = m_arraysize;
      }

      Box[][] result;

      Box[] row = fetchone ();

      /* Create and populate the array if a row is available.
       */
      if (row !is null) {
         
         result = new Box[][](size);

         int i = 0;

         /* Fill the array with available rows.
          */
         while (row !is null) {
            result[i++] = row;
            if (i == size) break;
            row = fetchone ();
         }

         if (i < size) {
            /* The value of i will be one greater than the index of the last row
             * due to the i++ when assigning the row thus the value of i is the
             * actual number of rows inserted into the array.
             */
            result.length = i;
         }
      }

      return result;
   }


   public Box[][] fetchall ()
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosing (m_connection);

      Box[][] result;

      int i = 0;
      Box[] row = fetchone ();

      while (row !is null) {
         if (i % m_arraysize == 0) {
            result.length = result.length + m_arraysize;
         }
         result[i++] = row;
         row = fetchone ();
      }

      if (result.length != i) {
         /* The value of i will be one greater than the index of the last row
          * due to the i++ when assigning the row thus the value of i is the
          * actual number of rows inserted into the array.
          */
         result.length = i;
      }

      return result;
   }


   /**
    * Move the cursor to the next unread row, or, when a row is unavailable,
    * return false.
    *
    * When the method returns true you can retrieve the row with fetchrow.
    * This method is intended for use with fetchrow and fetchstruct, do not
    * mix this method with fetchone as that method will also cause the cursor
    * to move.
    *
    * Returns:
    *    true when a row is available, otherwise false.
    *
    */
   public bool next () {
      return moveCursorToNextAvailableRecord ();
   }


   /**
    * Read only property returns true when the last database step
    * returned a row.
    *
    * In almost all cases, when this property is true, next() will return true.
    * The exception is after a call to fetchmanyrows, or fetchmanystructs, when
    * the last array element corresponds to the last row of the query. In that
    * case, the fetch loop will exit as the array is full but the database
    * step is not performed so the cursor pointer is pointing to the last
    * row instead of after the last row; then a call to next() will execute
    * a database step and return false.
    */
   public bool rowAvailable () {
      return m_rowAvailable;
   }


   /**
    * Type aware fetch from the database.
    *
    * With thanks to Chris Nicholson-Sauls, Lutger Blijdestijn,
    * and Christian Kam who provided code and information on how to make this
    * possible.
    *
    * Thanks to Kirk McDonald for the type aware fetch idea.
    */
   public Row!(T) fetchrow(T...) () {

      checkClosing (m_connection);

      if (!rowAvailable ()) {
         throw new SqlProgrammingException ("No data available");
      }

      Row!(T) r;

      int colcount = m_currentstmt.getRsColCount ();

      /* When the last step operation returned a record but the column count is
       * zero, something unexpected is happening so throw an exception.
       */
      if (0 == colcount) {
         m_rowAvailable = false;
         throw new SqlInterfaceException (
            "Unable to fetch a record. The operation to retrieve the next data "
            "row reported a row available but the number of columns reported "
            "is zero. This should never happen and is either a bug in this "
            "inteface, a bug in the SQLite implementation, or the way the "
            "SQLite implementation works has changed. Either way, unable to "
            "continue. ");

      } else if (colcount != r.c.length) {
         throw new SqlProgrammingException (
            "Number of types specified does not match the number of columns "
            "read from the query result.");
      }

      /* Retrieve one value for each type in the expression tuple and populate
       * the expression tuple.
       */
      foreach (i, v; r.c) {
         Box dbval = m_currentstmt.getValue (i);
         r.c[i] = unboxNullable!(typeof(r.c[i])) (dbval);
         r.nullFlag[i] = isNull (dbval);
      }

      return r;
   }


   public Row!(T)[] fetchmanyrows(T...) (int size = -1)
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosing (m_connection);

      if (-1 != size && 0 >= size) {
         throw new SqlInterfaceException (
            "Invalid size for fetchmanyrows. The size parameter must be either "
            "-1, to use arraysize, or a number greater than zero.");
      }

      if (-1 == size) {
         size = m_arraysize;
      }

      Row!(T)[] result;

      /* Create the result array if a row is available.
       */
      if (next ()) {

         result = new Row!(T)[](size);

         int i = 0;

         result[i++] = fetchrow!(T) ();
         
         while (i < size && next ()) {
            result[i++] = fetchrow!(T) ();
         }

         /* When the index (i) is less than the array size, reduce the array
          * size to the index value as the index value is the number of
          * rows populated in the result.
          */
         if (i < size) {
            /* The value of i will be one greater than the index of the last row
             * due to the i++ when assigning the row thus the value of i is the
             * actual number of rows inserted into the array.
             */
            result.length = i;
         }
      }

      return result;
   }


   public Row!(T)[] fetchallrows(T...) ()
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosing (m_connection);

      Row!(T)[] result;

      int i = 0;

      while (next ()) {
         if (i % m_arraysize == 0) {
            result.length = result.length + m_arraysize;
         }
         result[i++] = fetchrow!(T) ();
      }

      /* When the index (i) is less than the array size, reduce the array
       * size to the index value as the index value is the number of
       * rows populated in the result.
       */
      if (i < result.length) {
         /* The value of i will be one greater than the index of the last row
          * due to the i++ when assigning the row thus the value of i is the
          * actual number of rows inserted into the array.
          */
         result.length = i;
      }

      return result;
   }


   /**
    * Return a type aware record from the database and populate the struct.
    *
    * With thanks to Chris Nicholson-Sauls, Lutger Blijdestijn, and
    * Christian Kam.
    */
   T fetchstruct(T) () {

      static if (is (T == struct)) {

         T t;

         /* Check if last struct field is a bool array of correct size to store
          * null flags.
          */ 
         static if (is (typeof (t.tupleof[$-1]) == bool[t.tupleof.length-1])) {
            const bool hasNullFlags = true;
         } else {
            const bool hasNullFlags = false;
         }

         /* When the last field is a bool array of the correct size to store
          * null flags, exclude from the type tuple.
          */
         static if (hasNullFlags) {
            auto r = fetchrow!(FieldTypeTuple!(T)[0..length-1])();

         } else {
            auto r = fetchrow!(FieldTypeTuple!(T))();
         }

         /* Populate the structure from the fetched data.
          */
         foreach (i, v; r.c) {
            // According to Christian, using v may not work every time.
            t.tupleof[i] = r.c[i];
         }

         /* If the structure contains nullFlags, populate it.
          */
         static if (hasNullFlags) {

            foreach (i, nullFlagValue; r.nullFlag) {
               /* I use the name of the field instead of tupleof to guarantee
                * the name so helper functions can be written.
                */
               t.nullFlag[i] = nullFlagValue;
            }
         }
    
         return t;

      } else {
         static assert (0, "Invalid type, must be a struct.");
      }
   }


   public T[] fetchmanystructs(T) (int size = -1)
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosing (m_connection);

      if (-1 != size && 0 >= size) {
         throw new SqlInterfaceException (
            "Invalid size for fetchmanystructs. The size parameter must be "
            "either -1, to use arraysize, or a number greater than zero.");
      }

      if (-1 == size) {
         size = m_arraysize;
      }

      T[] result;

      /* Create the result array if a row is available.
       */
      if (next ()) {

         result = new T[](size);

         int i = 0;

         result[i++] = fetchstruct!(T) ();
         
         while (i < size && next ()) {
            result[i++] = fetchstruct!(T) ();
         }

         /* When the index (i) is less than the array size, reduce the array
          * size to the index value as the index value is the number of
          * rows populated in the result.
          */
         if (i < size) {
            /* The value of i will be one greater than the index of the last row
             * due to the i++ when assigning the row thus the value of i is the
             * actual number of rows inserted into the array.
             */
            result.length = i;
         }
      }

      return result;
   }


   public T[] fetchallstructs(T) ()
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosing (m_connection);

      T[] result;

      int i = 0;

      while (next ()) {
         if (i % m_arraysize == 0) {
            result.length = result.length + m_arraysize;
         }
         result[i++] = fetchstruct!(T) ();
      }

      /* When the index (i) is less than the array size, reduce the array
       * size to the index value as the index value is the number of
       * rows populated in the result.
       */
      if (i < result.length) {
         /* The value of i will be one greater than the index of the last row
          * due to the i++ when assigning the row thus the value of i is the
          * actual number of rows inserted into the array.
          */
         result.length = i;
      }

      return result;
   }



   public void close () {
      if (m_currentstmt !is null) {
         delete m_currentstmt;
      }
   }


   //---------------------------------------------------------------------------
   // Private methods
   //---------------------------------------------------------------------------


   private bool connectionOpen () {
      if (m_connection.isClosed ()) {
         close ();
         return false;
      } else {
         return true;
      }
   }


   public void printRawAttributes () {

      int colcount = m_currentstmt.getRsColCount ();

      for (int i = 0; i < colcount; i++) {
         writefln (
            "%s ::> %d", m_currentstmt.getStmtColName (i),
            m_currentstmt.getRsColType (i));
      }
   }


   /**
    * Extract the type name, precision and scale from the type declaration
    * string.
    *
    * The declaration string is an exact copy of the text used to create the
    * table column including all whitespace. Expected template is as follows:
    *
    *   type ::= typename | typename (number) | typename (number,number) 
    *
    * The first bracket found must be the start of precision.
    *
    * Notes:
    *   - SQLite places no restriction on the type name so this could be
    *     anything.
    *   - SQLite does not check that the scale fits within the precision thus
    *     you could have a scale greater or equal to the precision.
    */
   private void fillTypeFromStmtColType (
         char[] stmtColType, inout ColumnAttributes attr) {

      bool found (int idx) {return idx != -1;}

      //writefln ("COLTYPE ==> %s", stmtColType);

      int idxBracketStrt = find (stmtColType, '(');
      int idxBracketEnd  = find (stmtColType, ')');

      if (found (idxBracketStrt)) {
         if (!found (idxBracketEnd)) {
            throw new SqlInterfaceException (
               "Unable to process column type string '"~stmtColType~"' as "
               "encountered start bracket but no end bracket. This is "
               "unexpected and may indicate that the SQLite database is "
               "corrupt, or the engine is corrupt, or the engine column type "
               "function has changed. Unable to continue.");
         }

         char[][] restrictionStr =
            split (stmtColType[(idxBracketStrt+1) .. idxBracketEnd], ",");

         attr.typeCode  = strip (stmtColType[0 .. idxBracketStrt]);
         attr.precision = atoi (strip (restrictionStr[0]));
         if (restrictionStr.length > 1)
            attr.scale = atoi (strip (restrictionStr[1]));

      } else {

         attr.typeCode = strip (stmtColType);

      }
   }

   //---------------------------------------------------------------------------
   // Section: Parameter binding
   //---------------------------------------------------------------------------

   private void bindParameters (TypeInfo[] _arguments, va_list _argptr) {
      Box[] values = boxArray (_arguments, _argptr);
      bindParameters (values);
   }


   private void bindParameters (Box[] values) {

      /* Clear the current bindings on a statement so if it was reused, and
       * the user forgot to bind a value to a parameter, that parameter would
       * have the default (NULL) value rather than the previous bound
       * value. If this is a new statement, then the clearBindings will have no
       * effect.
       */
      m_currentstmt.clearBindings ();

      TypeInfo t = typeofNestedBox (values[0]);

      if (t == typeid (NamedParameter)) {
         bindNamedParameters (values);

      } else {
         bindPositionalParameters (values);
      }
   }


   /**
    * For nested boxes, retrieve the type of the boxed value.
    *
    * Returns:
    *    Type of the first non-box value.
    */
   private TypeInfo typeofNestedBox (Box value) {

      return nestedBox (value).type;
   }


   /**
    * For nested boxes, retrieve the innermost box.
    *
    * Returns:
    *    The innermost box value.
    */
   private Box nestedBox (Box value) {

      Box v = value;

      while (v.type == typeid (Box)) {
         v = unbox!(Box)(v);
      }

      return v;
   }


   private void bindPositionalParameters (Box[] values) {

      foreach (i, value; values) {

         /* A value may be contained within nested boxes. This could be for
          * many reasons but the one I envision is the case where the value is
          * retrieved by a query. Thus we allow the developer to copy a box
          * returned by the fetch* statement to the execute.
          */
         Box v = nestedBox (value);

         if (v.type == typeid (NamedParameter)) {

            throw new SqlProgrammingException (
               "Invalid bind type. A named parameter instance was specified "
               "when positional binding is being performed. Mixing of named "
               "and positional binding is invalid. Error on parameter (" ~
               str (i) ~ "). SQL (" ~ m_currentstmt.text ~ ").");
            
         }
         
         bindParameter (i, v);
      }
   }


   private void bindNamedParameters (Box[] values) {

      foreach (i, v; values) {

         /* When binding with named parameters, nested boxes are not allowed;
          * only type NamedParameters are allowed.
          */
         if (v.type != typeid (NamedParameter)) {

            if (v.type == typeid (Box)) {
               throw new SqlProgrammingException (
                  "Invalid bind type. Nested boxes are not allowed when named "
                  "binding is being performed. Error on parameter (" ~
                  str (i) ~ "). SQL (" ~ m_currentstmt.text ~ ").");

            } else {
               throw new SqlProgrammingException (
                  "Invalid bind type. A non-named parameter instance was "
                  "specified when named binding is being performed. Mixing of "
                  "named and positional binding is invalid. Error on parameter (" ~
                  str (i) ~ "). SQL (" ~ m_currentstmt.text ~ ").");
            }
         }

         NamedParameter p = unbox!(NamedParameter)(v);

         assert (p.value.type != typeid (NamedParameter),
            "A named parameter cannot have a named parameter as a value. "
            "Check that the constructor/opCall for NamedParameter enforces "
            "this assumption.");
         
         assert (p.value.type != typeid (Box),
            "A named parameter cannot have a box for a value. "
            "Check that the constructor/opCall for NamedParameter unnests "
            " boxed values.");

         bindParameter (p.name, p.value);
      }
   }


   private void bindParameter (char[] name, Box value) {
      int i = m_currentstmt.parmIndexFromName (name);
      bindParameter (i, value);
   }


   private void bindParameter (int i, Box value)
   in {
      assert (value.type != typeid (Box),
         "Value box may not contain nested boxes.");
   }
   body {
      alias value v;

      if (v.type == typeid (int)) {
         m_currentstmt.bindInt (i, unbox!(int)(v));

      } else if (v.type == typeid (long)) {
         m_currentstmt.bindLong (i, unbox!(long)(v));

      } else if (v.type == typeid (char[])) {
         m_currentstmt.bindUtf8 (i, unbox!(char[])(v));

      } else if (v.type == typeid (wchar[])) {
         m_currentstmt.bindUtf16 (i, unbox!(wchar[])(v));

      } else if (v.type == typeid (void*)) {
         if (isNull (v)) {
            m_currentstmt.bindNull (i);
            
         } else {
            throw new SqlProgrammingException (
               "Invalid bind value. Received non-null ptr type void* but "
               "only support void* value of null.");
         }

      } else if (v.type == typeid (void[])) {
         m_currentstmt.bindBlob (i, unbox!(void[])(v));

      } else if (v.type == typeid (float)) {
         m_currentstmt.bindDouble (i, unbox!(float)(v));

      } else if (v.type == typeid (double)) {
         m_currentstmt.bindDouble (i, unbox!(double)(v));

      } else {
         throw new SqlProgrammingException (
            "Invalid bind type. Received type that cannot be bound. Only "
            "types supported are [null, int, long, char[], wchar[], float, "
            "double, void[] (blob)]. Received type (" ~ v.type.toString() ~
            ").");
      }
   }
}


private enum StatementType {
     /**
      * The query statement. No data is modified and, when in transaction mode,
      * no transaction is started.
      */
     STATEMENT_QUERY

     /**
      * Data will be modified by this statement. When in transaction mode, if a
      * transaction has not been started, a new transaction is started.
      * The conclusion of the statement may cause the transaction to be ended.
      */
   , STATEMENT_MODIFY

     /**
      * A transaction statement: BEGIN, COMMIT, ROLLBACK.
      */
   , STATEMENT_TRANSACTION

     /**
      * All other statement types such as DDL, transaction, pragmas etc.
      */
   , STATEMENT_OTHER
}


/**
 * Creates a statement.
 *
 * Only single query statements are supported.
 */
private class Statement {
   private Connection      m_connection;
   private sqlite3_stmt*   m_stmt;
   private char[]          m_text;
   private int             m_compiledColumnCount;
   private int             m_resultsetColumnCount;
   private StatementType   m_statementType;
   private int             m_numBindParameters;

   this (Connection connection, char[] text) 
   in {
      assert (connection.isOpen (),
         "Cannot create statement of a closed connection.");
   }
   body {
      m_connection = connection;
      m_text       = text;

      /* Determine the statement type.
       */
      m_statementType = determineStatementType (text);

      if (StatementType.STATEMENT_TRANSACTION == m_statementType) {
         if (TransactionMode.USER != m_connection.m_transactionMode) {
            throw new SqlInterfaceException (
               "Invalid statement. A transaction statement (BEGIN, COMMIT, "
               "ROLLBACK) is not valid when the connection transaction mode is "
               "other than USER. SQL (" ~ text ~ ").");
         }
      }

      char *pzTail;

      int rc =
         sqlite3_prepare_v2 (
            connection.dbcon, toStringz (text), text.length, &m_stmt, &pzTail);

      switch (rc) {
         case SQLITE_OK:
            break;

         case SQLITE_ERROR:
            throw SqliteError!(SqlProgrammingException) (
               connection.dbcon,
               "Unable to create statement '" ~ text ~ "'. "
               "SQL error ($(errmsg)).");

         default:
            throw SqliteError!(SqlOperationalException) (
                  connection.dbcon,
                  "Unable to create statement '" ~ text ~ "'. "
                  "Sqlite error ($(errmsg)).");
      }

      // Test whether the input operation text contains a statement separator.
      // Since multiple operations per statement are not allowed, the presence
      // of the separator is illegal. The variable pzTail will only point to a
      // remainder if the statement separator is parsed.

      if (pzTail !is null && strlen (pzTail) > 0) {
         throw new SqlInterfaceException (
            "Illegal SQL text passed to create statement instance. "
            "Statements only support single operations but the supplied "
            "SQL text contains the operation separator ';' and is thus "
            "invalid.");
      }

      /* Calculate the compiled column count for the statement and cache the
       * result.
       */
      m_compiledColumnCount = sqlite3_column_count (m_stmt);

      /* Request the number of bind parameters.
       */
      m_numBindParameters = sqlite3_bind_parameter_count (m_stmt);
   }

   ~this () {
      close ();
   }

   /**
    * Execute the statement.
    *
    * Returns:
    *    true when a resultset is available, else false.
    */
   bool step ()
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");

      assert (m_stmt !is null,
         "Unable to execute statement as Statement instance is closed.");
   }
   body {
      int rc = sqlite3_step (m_stmt);

      m_resultsetColumnCount = sqlite3_data_count (m_stmt);

      switch (rc) {
         case SQLITE_DONE:
            return false;

         case SQLITE_ROW: 
            return true;

         case SQLITE_CONSTRAINT:
            //TODO check if still in transaction
            throw SqliteError!(SqlIntegrityException) (
               m_connection.dbcon,
               "Error on executing operation step '" ~ m_text ~ "'. "
               "The operation caused a constraint violation ($(errmsg)).");

         case SQLITE_SCHEMA:
            throw SqliteError!(SqlInternalException) (
               m_connection.dbcon,
               "Error on executing operation step '" ~ m_text ~ "'. "
               "The step could not be performed as the database schema has "
               "changed. "
               "Sqlite error ($(errmsg)).");

         case SQLITE_BUSY:
            throw SqliteError!(SqlOperationalException) (
               m_connection.dbcon,
               "Error on executing operation step '" ~ m_text ~ "'. "
               "Unable to execute as the database is busy. This should only "
               "be possible in multi-threaded environment. This api does "
               "not support multi-threaded operation.");

         default:
            throw SqliteError!(SqlOperationalException) (
               m_connection.dbcon,
               "Error on executing operation step '" ~ m_text ~ "'. "
               "Sqlite error ($(errmsg)).");
      }
   }

	void reset () 
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");

      assert (m_stmt !is null,
         "Unable to reset statement as Statement instance is closed.");
   }
   body {
      int rc = sqlite3_reset (m_stmt);

      if (rc != SQLITE_OK) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Error on statement reset for statement '" ~ m_text ~ "'. "
            "Sqlite error ($(errmsg)).");
      }

	}



   void close () {
      if (m_stmt !is null) {
         if (m_connection.isOpen ()) {
            int rc = sqlite3_finalize (m_stmt);

            switch (rc) {
               case SQLITE_OK:
               case SQLITE_ABORT:
                  // Closing is done after the block.
                  break;

               default:
                  throw SqliteError!(SqlOperationalException) (
                     m_connection.dbcon,
                     "Error on closing statement '" ~ m_text ~ "'. "
                     "Sqlite error ($(errmsg)).");
            }
         }
      }

      // Reset instance values to closed state.
      m_connection           = null;
      m_stmt                 = null;
      m_text                 = "";
      m_compiledColumnCount  = typeof (m_compiledColumnCount).init;
      m_resultsetColumnCount = typeof (m_resultsetColumnCount).init;
   }


   bool isOpen () {
      return m_connection.isOpen() && m_stmt !is null;
   }


   bool isClosed () {
      return m_connection.isClosed() || m_stmt is null;
   }


   sqlite3_stmt* stmt () 
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");

      assert (m_stmt !is null,
         "Unable to retrieve statement as Statement instance is closed.");
   }
   body {
      return m_stmt;
   }


   char[] text ()
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");

      assert (m_stmt !is null,
         "Unable to retrieve statement text as Statement instance is "
         "closed.");
   }
   body {
      return m_text;
   }


   StatementType statementType () {
      return m_statementType;
   }
   

   /**
    * Returns the number of bind parameters.
    *
    * Will return 0 when the statement has no bind parameters otherwise a
    * positive number is returned.
    */
   int numBindParameters () {
      return m_numBindParameters;
   }


   /**
    * Returns the number of records touched by the last statement executed.
    */
   int numChanges ()
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");

      assert (m_stmt !is null,
         "Unable to retrieve statement text as Statement instance is "
         "closed.");
   }
   body {
      if (StatementType.STATEMENT_MODIFY == m_statementType)
         return sqlite3_changes (m_connection.dbcon);
      else
         return 0;
   }


   //---------------------------------------------------------------------------
   // Section: Variable binding
   //
   // The following allow for binding values to a statement.
   //---------------------------------------------------------------------------

   void clearBindings () {

      int rc = sqlite3_clear_bindings (m_stmt);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to clear bindings. Sqlite error ($(errmsg), $(errcde)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }

   int parmIndexFromName (char[] name) {
      
      /* We have to prepend the ":" as sqlite stores the parameter name as-is
       * including the ":".
       */
      int position = sqlite3_bind_parameter_index (m_stmt, toStringz (":"~name));

      // When the bind name does not exist, throw an error.
      if (0 == position) {
         throw new SqlProgrammingException (
            "Parameter name not found. The SQL statement does not define a "
            "parameter name (" ~ name ~ "). SQL (" ~ m_text ~ ").");
         
      } else {
         
         // Decrement by 1 as sqlite indexing is 1-based but interface indexing
         // is 0-based.
         return position - 1;
      }
   }


   void bindBlob (int position, void[] value)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_blob (
         m_stmt, position + 1, value.ptr, value.length,
         SQLITE_TRANSIENT);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind blob parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }

   
   void bindDouble (int position, double value)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_double (m_stmt, position + 1, value);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind double parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }


   void bindInt (int position, int value)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_int (m_stmt, position + 1, value);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind int parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }


   void bindLong (int position, long value)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_int64 (m_stmt, position + 1, value);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind int64 parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }


   void bindNull (int position)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_null (m_stmt, position + 1);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind NULL parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }


   void bindUtf8 (int position, char[] value)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_text (
         m_stmt, position + 1, toStringz (value), value.length,
         SQLITE_TRANSIENT);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind UTF8 text parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }


   void bindUtf16 (int position, wchar[] value)
   in {
      assert (position >= 0,
         "Invalid position value (" ~ str (position) ~ "). Must be >= 0.");
      assert (position < m_numBindParameters,
         "Invalid position value (" ~ str (position) ~
         "). Must be < m_numBindParameters (" ~ str (m_numBindParameters) ~ ").");
   }
   body {
      int rc = sqlite3_bind_text16 (
         m_stmt, position + 1, value.ptr, value.length * 2,
         SQLITE_TRANSIENT);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException) (
            m_connection.dbcon,
            "Failed to bind UTF16 text parameter. Error on parameter (" ~
            str (position) ~ "). Sqlite error ($(errmsg)). SQL '" ~
            m_text ~ "'. "
         );
      }
   }


   //---------------------------------------------------------------------------
   // Section: Compiled Statement Meta Information Methods
   //
   // The following methods query the compiled statement meta information.
   //---------------------------------------------------------------------------

   /**
    * Returns the number of columns expected by the compiled SQL statement.
    */
   int getStmtColCount ()
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");

      assert (m_stmt !is null,
         "Unable to retrieve column count as Statement instance is closed.");
   }
   body {
      return m_compiledColumnCount;
   }



   char[] getStmtColName (int colnum)
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");
      assert (m_stmt !is null,
         "Unable to retrieve column name as Statement instance is closed.");
      assert (colnum >= 0,
         "Unable to get column name as colnum (" ~ str (colnum) ~
         ") is a negative number. Only positive column numbers are supported.");
      assert (colnum < m_compiledColumnCount,
         "Unable to get column name as colnum (" ~ str (colnum) ~
         ") > available columns (" ~ str (m_compiledColumnCount) ~ ")");
   }
   body {
      char* name = sqlite3_column_name (m_stmt, colnum);
      return str (name);
   }



   char[] getStmtColType (int colnum)
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");
      assert (m_stmt !is null,
         "Unable to retrieve column type as Statement instance is closed.");
      assert (colnum >= 0,
         "Unable to get column type as colnum (" ~ str (colnum) ~
         ") is a negative number. Only positive column numbers are supported.");
      assert (colnum < m_compiledColumnCount,
         "Unable to get column type as colnum (" ~ str (colnum) ~
         ") > available columns (" ~ str (m_compiledColumnCount) ~ ")");
   }
   body {
      char* type = sqlite3_column_decltype (m_stmt, colnum);
      if (null == type) {
         return "EXPRESSION";
      } else {
         return str (type);
      }
   }

   //---------------------------------------------------------------------------
   // Section: Result Set Meta Information Methods
   //
   // The following methods query the result set meta information.
   //---------------------------------------------------------------------------

   /**
    * Returns the number of columns returned by the last step operation.
    */
   int getRsColCount ()
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");
      assert (m_stmt !is null,
         "Unable to retrieve resultset column count as Statement instance is "
         "closed.");
   }
   body {
      return m_resultsetColumnCount;
   }

   

   int getRsColType (int colnum)
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");
      assert (m_stmt !is null,
         "Unable to retrieve column type as Statement instance is closed.");
      assert (colnum >= 0,
         "Unable to get column type as colnum (" ~ str (colnum) ~
         ") is a negative number. Only positive column numbers are supported.");
      assert (colnum < m_resultsetColumnCount,
         "Unable to get column type as colnum (" ~ str (colnum) ~
         ") > available columns (" ~ str (m_resultsetColumnCount) ~ ")");
   }
   body {
      return sqlite3_column_type (m_stmt, colnum);
   }

   //---------------------------------------------------------------------------
   // Section: 
   //---------------------------------------------------------------------------

   Box getValue (int colnum)
   in {
      assert (m_connection.isOpen (),
         "Operation on cannot be done as connection is closed.");
      assert (m_stmt !is null,
         "Unable to retrieve column value as Statement instance is closed.");
      assert (colnum >= 0,
         "Unable to get column value as colnum (" ~ str (colnum) ~
         ") is a negative number. Only positive column numbers are supported.");
      assert (colnum < m_resultsetColumnCount,
         "Unable to get column value as colnum (" ~ str (colnum) ~
         ") > available columns (" ~ str (m_resultsetColumnCount) ~ ")");
   }
   body {
      Box value;

      int type = sqlite3_column_type (m_stmt, colnum);

      switch (type) {
         case SQLITE_INTEGER:
            int v = sqlite3_column_int (m_stmt, colnum);
            value = box (v);
            break;

         case SQLITE_FLOAT:
            double v = sqlite3_column_double (m_stmt, colnum);
            value = box (v);
            break;


         case SQLITE_TEXT:
            char* v = sqlite3_column_text (m_stmt, colnum);
            /* The .dup is necessary as toString(char*) does not copy the
             * contents of the char*, it just wraps it. Without the clone, 
             * every text box returned by this function would display as
             * the last string constructed.
             */
            value = box (str (v).dup);
            break;


         case SQLITE_BLOB:
            void* v = sqlite3_column_blob (m_stmt, colnum);
            int size = sqlite3_column_bytes (m_stmt, colnum);
            value = box (v [0 .. size].dup);
            break;

         case SQLITE_NULL:
            value = box (null);
            break;

         default:
            throw new SqlInterfaceException (
               "Unable to retrieve value for column (" ~ str (colnum) ~ "). "
               "Do not know how to handle unexpected type code (" ~ str (type) ~
               "). Error on statement '" ~ m_text ~ "'.");

      }

      return value;
   }

   //---------------------------------------------------------------------------
   // Section: 
   //---------------------------------------------------------------------------

   version (sqlite_enable_column_metadata) {
      char[] getColumnDatabaseName (int colnum) {
         char* name = sqlite3_column_database_name (m_stmt, colnum);
         if (null == name) {
            return "";
         } else {
            return str (name);
         }
      }

      char[] getColumnTableName (int colnum) {
         char* name = sqlite3_column_table_name (m_stmt, colnum);
         if (null == name) {
            return "";
         } else {
            return str (name);
         }
      }
   }


   //---------------------------------------------------------------------------
   // Section: 
   //---------------------------------------------------------------------------


   /**
    * Parse the provided statement text and determine the type.
    *
    * The statement may be one of the following:
    *
    *   STATEMENT_QUERY   - The select statement is a query statement.
    *   STATEMENT_MODIFY  - The insert, update, replace, and delete statements
    *                       are statements that modify data.
    *   STATEMENT_OTHER   - All other statements such as create, drop,
    *                       pragma, begin, commit, etc.
    *
    * To determine the statement type, leading whitespace is ignored and the
    * first word is read and mapped to the statement type. Leading comments will
    * cause an error to be thrown. An empty statement will cause an invalid
    * statement exception to be thrown.
    */
   private StatementType determineStatementType (char[] text) {
      
      StatementType st;

      char[] operation = strip (text);

      if (operation.length == 0) {
         throw new SqlProgrammingException (
            "Invalid SQL statement, expecting an operation but received " ~
            (text.length == 0
               ? "an empty string." :
                 "a string consisting of only white spaces."));
      }

      /* Find the end of the first word.
       */
      int i = 0;
      while (i < operation.length && !iswhite (operation[i])) {
         i++;
      }

      /* If the first word is the only word, then the word cannot be a QUERY or
       * MODIFY operation so it is an OTHER operation.
       *
       * If the first word does not end on the operation string boundry, then
       * splice out the first word and uppercase then test for type.
       */
      if (i < operation.length) {
         operation = toupper (operation [0 .. i]);

         if (operation == "SELECT") {
            st = StatementType.STATEMENT_QUERY;
            
         } else if (inAr (operation, ["INSERT", "UPDATE", "REPLACE", "DELETE"])) {
            st = StatementType.STATEMENT_MODIFY;

         } else if (inAr (operation, ["BEGIN", "END", "COMMIT", "ROLLBACK"])) {
            st = StatementType.STATEMENT_TRANSACTION;

         } else {
            st = StatementType.STATEMENT_OTHER;
         }

      } else {
         st = StatementType.STATEMENT_OTHER;
      }

      return st;
   }
}


/**
 * A statement wrapper for the transaction statements BEGIN, COMMIT, ROLLBACK.
 *
 * There is a special case for COMMIT where the statement will return a
 * non-zero return code but must not 
 *
 */
private class TransactionStatement {

   this (sqlite3* dbcon, char[] text) {
      int rc =
         sqlite3_prepare_v2 (
            dbcon, toStringz (text), text.length, &m_stmt, null);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException)(
            dbcon,
            "Unable to create transaction statement. "
            "Sqlite error ($(errmsg)). "
            "SQL (" ~ text ~ ").");
      }

      m_text  = text;
      m_dbcon = dbcon;
   }

   ~this() {
      debug {
         writefln ("Transaction statement destructing");
      }

      int rc = sqlite3_finalize (m_stmt);

      switch (rc) {
         case SQLITE_OK:
         case SQLITE_ABORT:
            // Closing is done after the block.
            break;

         default:
            throw SqliteError!(SqlOperationalException) (
               m_dbcon,
               "Error on closing transaction statement. "
               "Sqlite error ($(errmsg))."
               "SQL (" ~ m_text ~ ").");
      }
   }

   /**
    * Execute the transaction statement.
    *
    * When the transaction statement is COMMIT, a SQLITE_BUSY code may be
    * returned. In that case, the method will return false.
    *
    * Returns:
    *   true when the statement executed without problems.
    *   false when the statement could not execute due to locked database.
    */
   bool exec () {
      int rc = sqlite3_reset (m_stmt);

      if (SQLITE_OK == rc) {
         rc = sqlite3_step (m_stmt);
         
         switch (rc) {
            case SQLITE_BUSY:
               return false;

            case SQLITE_DONE:
               rc = SQLITE_OK;

            default:
               // Do nothing to allow the error handler below to handle the
               // error.
         }
      }

      debug {
         writefln ("%X - %s: RC = %d", &this, m_text, rc);
      }

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException)(
            m_dbcon,
            "Unable to execute transaction statement. "
            "Sqlite error ($(errmsg), $(errcde)). "
            "SQL (" ~ m_text ~ ").");
      }
      
      return true;
   }

   private sqlite3_stmt*   m_stmt;
   private char[]          m_text;
   private sqlite3*        m_dbcon;
}


private char[] getApiErrorMessage (sqlite3 *dbcon) {
   char* errmsg = sqlite3_errmsg (dbcon);
   return toString (errmsg);
}

private struct DbError {
   char[] msg;
   int    code;
}

/**
 * Retrieve the error message and error code of the supplied sqlite connection.
 */
private DbError getApiError (sqlite3 *dbcon) {
   char* errmsg = sqlite3_errmsg (dbcon);
   int errcd    = sqlite3_errcode (dbcon);

   DbError e;
   e.msg = toString (errmsg);
   e.code = errcd;

   return e;
}

private DbError
formatExceptionMessageFromConnection (sqlite3 *dbcon, char[] msg) {

   DbError err = getApiError (dbcon);

   char[] newmsg;
   newmsg = replace (msg   , "$(errmsg)", err.msg);
   newmsg = replace (newmsg, "$(errcde)", toString (err.code));

   err.msg = newmsg;

   return err;
}

/**
 * Template to construct a dbapi exception containing sqlite error details.
 */
private T SqliteError (T)(sqlite3 *dbcon, char[] msg) {
   DbError formatted =
      formatExceptionMessageFromConnection (dbcon, msg);

   T e = new T (formatted.msg, null, formatted.code);

   return e;
}


private bool inAr(T) (T val, T[] ar) {
   foreach (T x ; ar) {
      if (val == x) return true;
   }
   return false;
}


private void checkClosing (Connection c) {
   if (c.m_isClosing) {
      throw new SqlInterfaceException (
         "The connection is closing. No further operations on the connection "
         "are permitted.");
   }
}


private va_list nextArg (TypeInfo t, va_list argptr) {
   return argptr + argumentLength (t.tsize());
}


/** Return the length of an argument in bytes. */
private size_t argumentLength (size_t baseLength)
{
   return (baseLength + int.sizeof - 1) & ~(int.sizeof - 1);
}


/**
 * Batch update exception composits underlying cause with array of update counts.
 * 
 * The update counts are for all commands that were executed successfully during
 * the batch update, that is, all commands that were executed before the error
 * occurred. The order of elements in an array of update counts corresponds to
 * the order in which commands were added to the batch. The last element is the
 * iteration of the error and will contain the value BatchResult.EXECUTE_FAILED.
 */
class BatchUpdateException : SqlException {

   this (Exception cause, int[] updateCounts)
   in {
      assert (cause !is null);
      assert (updateCounts !is null);
      assert (updateCounts.length > 0);
      assert (updateCounts[$] == BatchResult.EXECUTE_FAILED);
   }
   body {
      super (null);
      m_cause        = cause;
      m_updateCounts = updateCounts;
   }


   override char[] toString () {
      return m_cause.toString ();
   }


   override char[] sqlState () {

      if (cast(SqlException)m_cause) {
         return (cast(SqlException)m_cause).sqlState;

      } else {
         /* When the cause is not a SqlException, return the default value
          * as set by the super constructor.
          */
         return m_sqlState;
      }
   }


   override int vendorCode () {

      if (cast(SqlException)m_cause) {
         return (cast(SqlException)m_cause).vendorCode;

      } else {
         /* When the cause is not a SqlException, return the default value
          * as set by the super constructor.
          */
         return m_vendorCode;
      }
   }


   int[] updateCounts () {
      return m_updateCounts;
   }


   private Exception m_cause;
   private int[]     m_updateCounts;
}




interface TransactionModeHandlerFactory {
   TransactionModeHandler createTransactionModeHandler (
                                            sqlite3*        dbcon
                                          , TransactionMode transactionMode
                                          , TransactionType transactionType );
}



class StdTrMoHaFactory : TransactionModeHandlerFactory {

   override TransactionModeHandler createTransactionModeHandler (
                                            sqlite3*        dbcon
                                          , TransactionMode transactionMode
                                          , TransactionType transactionType )
   {
      switch (transactionMode) {

         case TransactionMode.USER:
            return new UserModeHandler (dbcon, transactionType);

         case TransactionMode.AUTO_COMMIT:
            return new AutoCommitModeHandler (dbcon, transactionType);

         case TransactionMode.ON_MODIFY:
            return new OnModifyModeHandler (dbcon, transactionType);

         case TransactionMode.ALWAYS:
            return new AlwaysModeHandler (dbcon, transactionType);

         default:
            throw new SqlInterfaceException (
               "Unknown Transaction Mode (" ~ str(cast(int)transactionMode) ~
               ").");
      }
   }
}




/**
 * Interface for handler classes that handle transaction events.
 *
 * The connection level events are implicit and handled when the class is
 * instantiated and destroyed.
 */
interface TransactionModeHandler {
   // Batch execution events
   void startBatch ();
   void batchError ();
   void endBatch ();

   // Non-Batch execution events
   void startExecute (StatementType statementType);
   void executionError ();
   void endExecute ();

   // Explicit termination of transaction events
   bool commit ();
   void rollback ();   
   
   // Transaction state
   bool inTransaction ();
}

private bool inTransactionImpl (sqlite3* dbcon) {
   /* In C, TRUE <> 0, FALSE == 0. The sqlite3_get_autocommit returns FALSE
    * when a transaction is in effect.
    */
   return sqlite3_get_autocommit (dbcon) == 0;
}

/**
 * Implements the handling of USER transaction mode.
 *
 * The handler does nothing as, by definition of USER transaction mode, the
 * user assumes the responsibility for transaction control.
 */
class UserModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (sqlite3* dbcon, TransactionType transactionType) {
      
      debug {
         writefln ("UserModeHandler instantiating");
      }

      m_dbcon = dbcon;
   };

   ~this () {
      debug {
         writefln ("UserModeHandler destructing");
      }
   }
   
   //
   // Batch execution events
   //

   override void startBatch () {
   }

   override void batchError () {
   }

   override void endBatch () {
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType) {
   }

   override void executionError () {
   }

   override void endExecute () {
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () {
      return true;
   }

   override void rollback () {
   }

   //
   // Transaction state
   //
   
   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }

   protected sqlite3* m_dbcon;
}

class AutoCommitModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (sqlite3* dbcon, TransactionType transactionType) {

      debug {
         writefln ("AutoCommitModeHandler instantiating");
      }

      m_dbcon = dbcon;

      m_commitStmt   = new TransactionStatement (m_dbcon, "commit");
      m_beginStmt    = new TransactionStatement (m_dbcon, "begin");
   };
   
   ~this () {
      debug {
         writefln ("AutoCommitModeHandler destructing");
      }
      delete m_commitStmt;
      delete m_beginStmt;
   }

   //
   // Batch execution events
   //

   override void startBatch ()
   in {
      assert (!inTransaction);
   }
   body {
      m_beginStmt.exec ();
   }

   override void batchError () {
      if (inTransaction) {
         m_commitStmt.exec ();
      }
   }

   override void endBatch ()
   in {
      assert (inTransaction);
   }
   body {
      m_commitStmt.exec ();
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType) {
   }

   override void executionError () {
   }

   override void endExecute () {
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () {
      return true;
   }

   override void rollback () {
   }

   //
   // Transaction state
   //
   
   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }



   protected sqlite3*               m_dbcon;
   protected TransactionStatement   m_commitStmt;
   protected TransactionStatement   m_beginStmt;
}


class OnModifyModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (sqlite3* dbcon, TransactionType transactionType) {

      debug {
         writefln ("OnModifyModeHandler instantiating");
      }

      m_dbcon = dbcon;

      m_commitStmt   = new TransactionStatement (m_dbcon, "commit");
      m_rollbackStmt = new TransactionStatement (m_dbcon, "rollback");

      switch (transactionType) {
         case TransactionType.DEFAULT:
            m_beginStmt = new TransactionStatement (m_dbcon, "begin");
            break;

         case TransactionType.DEFERRED:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin deferred");
            break;
         
         case TransactionType.IMMEDIATE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin immediate");
            break;

         case TransactionType.EXCLUSIVE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin exclusive");
            break;

         default:
            throw new SqlInterfaceException (
               "Invalid transaction type (" ~
               str (cast(int)(transactionType)) ~ ").");
      }
   };

   ~this () {
      debug {
         writefln ("OnModifyModeHandler destructing");
      }

      delete m_commitStmt;
      delete m_rollbackStmt;
      delete m_beginStmt;
   }

   //
   // Batch execution events
   //

   override void startBatch () {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   override void batchError () {
   }

   override void endBatch () {
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType) {

      switch (statementType) {

         case StatementType.STATEMENT_MODIFY:
            if (!inTransaction) {
               m_beginStmt.exec ();
            }
            break;
         
         /* When the statement is other than a DQL, or DML statement, the
          * transaction must be committed to ensure consistent operation.
          * Example of 'other' statements are DDL, and pragma.
          */
         case StatementType.STATEMENT_OTHER:
            if (inTransaction) {
               m_commitStmt.exec ();
            }
            break;
         
         default:
            // Do nothing for other types
      }
   }

   override void executionError () {
   }

   override void endExecute () {
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () {
      if (inTransaction) {
         return m_commitStmt.exec ();
      } else {
         return true;
      }
   }

   override void rollback () {
      if (inTransaction) {
         m_rollbackStmt.exec ();
      }
   }

   //
   // Transaction state
   //
   
   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }


   protected sqlite3*               m_dbcon;
   protected TransactionStatement   m_commitStmt;
   protected TransactionStatement   m_beginStmt;
   protected TransactionStatement   m_rollbackStmt;
}

class AlwaysModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (sqlite3* dbcon, TransactionType transactionType) {

      debug {
         writefln ("AlwaysModeHandler instantiating");
      }

      m_dbcon = dbcon;

      m_commitStmt   = new TransactionStatement (m_dbcon, "commit");
      m_rollbackStmt = new TransactionStatement (m_dbcon, "rollback");

      switch (transactionType) {
         case TransactionType.DEFAULT:
            m_beginStmt = new TransactionStatement (m_dbcon, "begin");
            break;

         case TransactionType.DEFERRED:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin deferred");
            break;
         
         case TransactionType.IMMEDIATE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin immediate");
            break;

         case TransactionType.EXCLUSIVE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin exclusive");
            break;

         default:
            throw new SqlInterfaceException (
               "Invalid transaction type (" ~
               str (cast(int)(transactionType)) ~ ").");
      }
   
      m_beginStmt.exec ();
   };

   ~this () {
      debug {
         writefln ("AlwaysModeHandler destructing");
      }

      delete m_commitStmt;
      delete m_rollbackStmt;
      delete m_beginStmt;
   }

   //
   // Batch execution events
   //

   override void startBatch ()
   in {
      assert (inTransaction);
   }
   body {
   }

   override void batchError ()
   out {
      assert (inTransaction);
   }
   body {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   override void endBatch ()
   in {
      assert (inTransaction);
   }
   body {
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType)
   in {
      assert (inTransaction);
   }
   body {
      /* When the statement is other than a DQL, or DML statement, the
       * transaction must be committed to ensure consistent operation.
       * Example of 'other' statements are DDL, and pragma.
       */
      if (StatementType.STATEMENT_OTHER == statementType) {
         m_commitStmt.exec ();
      }
   }

   override void executionError ()
   out {
      assert (inTransaction);
   }
   body {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   override void endExecute ()
   out {
      assert (inTransaction);
   }
   body {
      /* When the statement is other than a DQL, or DML statement, the
       * startExecute would have committed the transaction before the
       * statement is executed. We must restore the transaction if that has
       * happened.
       */
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () 
   in {
      assert (inTransaction);
   }
   out {
      assert (inTransaction);
   }
   body {
      bool rc = m_commitStmt.exec ();
      if (rc) {
         rc = m_beginStmt.exec ();
         assert (rc == true);
      }
      return rc;
   }

   override void rollback ()
   in {
      assert (inTransaction);
   }
   out {
      assert (inTransaction);
   }
   body {
      bool rc = m_rollbackStmt.exec ();
      assert (rc == true);
      rc = m_beginStmt.exec ();
      assert (rc == true);
   }

   //
   // Transaction state
   //
   
   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }


   protected sqlite3*               m_dbcon;
   protected TransactionStatement   m_commitStmt;
   protected TransactionStatement   m_beginStmt;
   protected TransactionStatement   m_rollbackStmt;
}





//------------------------------------------------------------------------------
// Module static variables and initialisation
//------------------------------------------------------------------------------

static TransactionModeHandlerFactory   standardModeHandlerFactory;

static this() {
   standardModeHandlerFactory = new StdTrMoHaFactory ();
}

static ~this() {
   delete standardModeHandlerFactory;
}

// vim:et:sw=3:ts=3:
